import java.lang.reflect.*;
import java.util.*;
import java.net.*;
import java.io.*;

/*
 *  @author  Alpesh Mistry  <apm1dec@gmail.com>
 *  @author  Ankur Kothari  <a.kothari711@gmail.com>
 *  @author  Elman Mansimov <elman.mansimov@gmail.com>
 *  @author  Michelle Pan   <panmiche@gmail.com>
 *  @version 1.0
 *  @since   2012-03-03 
 */

public class FileSystem {
	
	/**
	 * Creates a new Directory that where all the directories 
	 * and files are stored. It doesn't have any parent. 
	 */
	private static Directory root = new Directory("root", null);
	
	/**
	 * Creates a new Directory that stores the location of 
	 * the current directory 
	 */
	public static Directory currDir = root;
	
	/**
	 * Creates a new FilesSystem that is used to make instance
	 * methods. 
	 */
	private Class<FileSystem> fs = FileSystem.class;
	
	/**
	 * Creates a new Hashtable which stores the command name
	 * as keys and method names that execute that command.
	 * The keys and method are also used to get the appropriate
	 * manual for the command.
	 */
	private Hashtable<String, String> cmdType = new Hashtable<String, String>();
	
	/**
	 * Stores an instance of FileSystem class from the variable fs,
	 * so that methods can be invoked inside it.
	 */

	private Object obj;

	/**
	 * Used to implement Singleton process on FileSystem.
	 */
	private static FileSystem onlyFileSystem = null;
	
	/*
	 * Used to call a method that can generate cmdType Hashtable. 
	 */

	public FileSystem() {
		
		// Generates commands for cmdType Hashtable
		this.generateCommands();
	}
	
	/**
	 * Used to implement so everyone can use the same FileSystem.
	 * 
	 * It synchronizes everyone using FileSystem. So multiple 
	 * copies or out of date copies are not used
	 */

	public static synchronized FileSystem getInstance() {
		 
		if( onlyFileSystem == null)
			 onlyFileSystem = new FileSystem();

	    return onlyFileSystem;
	}
	
	/**
	 * Prevents the cloning of FileSystem.
	 * 
	 * Prevents the cloning of FileSystem. and throws an Exception
	 * if anyone tries to do so. 
	 *
	 * @throws CloneNotSupportedException This is thrown if someone 
	 * 									  tries to create a copy of FileSystem.
	 */

	public FileSystem clone() throws CloneNotSupportedException {
		 
	      throw new CloneNotSupportedException("FILESYSTEM DUPLICATION not allowed.");
	}
	
	/**
	 *	Enters data in Hashtable cmdType.
	 *
	 * This method is used so that it can keep a copy of the commands and 
	 * the methods they are supposed to execute. Also helps in getting the
	 * manuals for each command
	 */

	private void generateCommands() {
		
		cmdType.put("cd", "changeDirectory");
		cmdType.put("mkdir", "makeDirectory");
		cmdType.put("ls", "printItemLists");
		cmdType.put("pwd", "printCurrentDirectory");
		//cmdType.put("mv", "moveItem");
		//cmdType.put("cp", "copyItem");
		cmdType.put("cat", "printFileContents");
		cmdType.put("get", "printUrlData");
		cmdType.put("echo", "writeToFile");
		cmdType.put("ln", "linkTo");
		cmdType.put("man", "printManual");
		cmdType.put("exit", "exit");
		cmdType.put("rm", "removeItem");
		cmdType.put("sample", "sample");
	}
	
	/**
	 * Calls the method depending on the user input.
	 * 
	 * This method calls the methods that help execute the appropriate
	 * commands. If the method doesn't exist then an error is raised 
	 * a message is  printed. 
	 *
	 * @throws InstantiationException    If an instance of FileSystem can't be made.
	 * @throws IllegalAccessException    If the code tries execute a method,
	 *                                   does not exist in the class or is an abstract method
	 *                                   in the class.
	 * @throws SecurityException         If the code tries to access methods that it
	 * 									 is not allowed to. 
	 *                                   method or object that is set private
	 *                                   and doesn't give permission to access it.                                     
	 * @throws InvocationTargetException If the given method doesn't exist or can't be invoked.
	 * @see java.lang.reflect
	 */

	public void selectFunction(String[] cmdInput)
		throws 	InstantiationException,
				IllegalAccessException,
				SecurityException,
				IllegalAccessException,
				InvocationTargetException {
		
		Class<?> [] arguments = {String[].class};
		obj = fs.newInstance();
		Method func = null;

		try {
			
			func = fs.getDeclaredMethod(cmdType.get(cmdInput[0]), arguments);
			func.invoke(obj, (Object) cmdInput);

		} catch (Exception e) {
			
			System.out.println("Invalid command.");
		}
	}
	
	/**
	 * Helper method to print Lines to help better distinguish the manual.
	 * 
	 * Helper method to print Lines to help better distinguish the manual
	 */

	private void printLining() {
		
		System.out.print("__________________________________");
		System.out.print("__________________________________");
		System.out.print("__________________________________");
		System.out.println("__________________________________");
		System.out.println();
	}
	
	/**
	 * Prints the manual of the given command.
	 * 
	 * Prints the manual of the given command to help the user better understand it.
	 * Error is raised when a a command doesn't exist and an appropriate message is printed.
	 * 
	 * @param cmdHelp String array that stores the commands that the user has asked for.
	 */

	@SuppressWarnings("unused")
	private void printManual(String[] cmdHelp) {

		int i;
		try {
			for (i = 1; i < cmdHelp.length; i++)
				try {

					String fileName = "src/" + cmdType.get(cmdHelp[i]) + ".txt";
					FileInputStream fileInput = new FileInputStream(fileName);
					InputStreamReader fileRead = new InputStreamReader(fileInput);
					BufferedReader in = new BufferedReader(fileRead);
					printLining();

					while(in.ready())
						System.out.println(in.readLine());

					in.close();
					printLining();

				} catch(Exception e) {

					System.out.println();
					System.out.println(cmdHelp[i] + ": command does no exist.");
					System.out.println();
				}

			cmdHelp[1] = cmdHelp[1];

		} catch (Exception e) {
			System.out.println("What manual page do you want?");
		}
	}
	
	/**
	 * Prints the URL data of the given URL.
	 * 
	 * Prints the URL data. If "http://" doesn't exist before a link, it is added.
	 * Error is raised when the URL doesn't exist and an appropriate message is printed.
	 * 
	 * @param cmdUrl String array that stores the URL that the user has asked for.
	 */

	@SuppressWarnings("unused")
	private void printUrlData(String[] cmdUrl) {
		
		if (cmdUrl.length > 2)
			System.out.println("Too many arguments. Try 'man get' for more information.");
		else
			try {	
				if (!cmdUrl[1].startsWith("http://"))
					cmdUrl[1] = "http://" + cmdUrl[1];

				URL url_name = new URL(cmdUrl[1]);
				InputStreamReader urlRead = new InputStreamReader(url_name.openStream());				
				BufferedReader in = new BufferedReader(urlRead);

				while (in.ready()) 
					System.out.println(in.readLine());

				in.close();

			} catch(Exception e) {

					System.out.println("Invalid URL. Try 'man get' for more information.");
			} 
	}
	
	/**
	 * Prints the path of the working directory.
	 * 
	 * Prints the path of the working directory. 
	 * If the command is not given correctly an appropriate message is printed.
	 * 
	 * @param cmdUrl String array that stores the URL that the user has asked for.
	 */

	@SuppressWarnings("unused")
	private void printCurrentDirectory(String[] currDirPath) {

		if (currDirPath.length < 2)
			if (currDir.getParent() != null)
				 System.out.println(currDir.getFullPath());
			else
				System.out.println("/");
		
		else
			System.out.println("Too many arguments. Try 'man pwd' for more information.");
	}

	/**
	 * Changes the working directory.
	 * 
	 * Changes the working directory. 
	 * If the command is not given correctly an appropriate message is printed
	 * or/and error is raised.
	 * 
	 * @param changeDir String array that stores path that the user has asked for.
	 */

	@SuppressWarnings("unused")
	private void changeDirectory(String[] changeDir) {
		
		Directory temp = currDir;
		String[] dirPath = null;
		String[] newDirPath = null;

		if (changeDir.length == 1)
			currDir = root;

		else if (changeDir.length == 2) {

			dirPath = changeDir[1].trim().split("/");
			
			if (dirPath[0].equals("")) {
				
				currDir = root;
				newDirPath = new String[dirPath.length - 1];
				System.arraycopy(dirPath, 1, newDirPath, 0, newDirPath.length);
			
			} else {
				
				newDirPath = new String[dirPath.length];
				System.arraycopy(dirPath, 0, newDirPath, 0, newDirPath.length);
			}
			currDir = changeDirHelper(newDirPath, currDir);
			if (currDir == null){
				currDir = temp;
				System.out.println(changeDir[1] + " : Invalid path");
			}
		} else
			System.out.println("Invalid arguments. Try 'man cd' for help.");
	}

	private Directory changeDirHelper(String[] thePath, Directory tempDir) {
		
		String[] thePath2 = null;

		if (thePath.length == 0)
			return tempDir;
		else
			try {
				tempDir = (Directory) tempDir.getContents().get(thePath[0]);
				thePath2 = new String[thePath.length-1];
				System.arraycopy(thePath, 1, thePath2, 0, thePath2.length);
				return changeDirHelper(thePath2, tempDir);
			} catch (Exception e) {
				return null;
		}
	}

	/**
	 * Creates a new directory.
	 * 
	 * Creates a new directory in the working directory or the given path 
	 * If the command is not given correctly an appropriate message is printed
	 * or/and error is raised.
	 * 
	 * @param cmdMake String array that stores the paths and directories to be created.
	 */

	@SuppressWarnings("unused")
	private void makeDirectory(String[] cmdMake) {

		Directory temp = currDir;
		String[] dirPath = null;
		String[] newDirPath = null;
		String dirName = null;
		int i;

		try {
			for (i = 1; i < cmdMake.length; i++) {
				
				dirPath = cmdMake[i].trim().split("/");
				dirName = dirPath[dirPath.length - 1];
				try {
					
					if (dirPath.length > 1)
						if (dirPath[0].equals("")) {

							newDirPath = new String[dirPath.length - 2];
							System.arraycopy(dirPath, 1, newDirPath, 0, newDirPath.length);
							currDir = changeDirHelper(newDirPath, root);
						
						} else {
							
							newDirPath = new String[dirPath.length - 1];
							System.arraycopy(dirPath, 0, newDirPath, 0, newDirPath.length);
							currDir = changeDirHelper(newDirPath, currDir);
						}

					if (!currDir.getContents().containsKey(dirName))
						currDir.addItem(dirName, new Directory(dirName, currDir));
					
					else {
						System.out.print("Cannot create directory. " + dirName);
						System.out.println(" directory already exists.");
					}

					currDir = temp;
					dirPath = null;
					newDirPath = null;

				} catch (Exception e) {

					System.out.print("Cannot create directory. `"+cmdMake[i]);
					System.out.println("\"" + ": No such directory exists.");
					currDir = temp;
					dirPath = null;
					newDirPath = null;
				}
			}

			cmdMake[1] = cmdMake[1];

		} catch (Exception e) {

			System.out.println("Invalid argument. Try 'man mkdir' for help.");
		}
	}

	/**
	 * Prints the contents of the directory or a file depending on user input.
	 *  
	 * Prints the contents of the directory or a file depending on user input 
	 * which is located in the working directory or the given path. 
	 * If the command is not given correctly an appropriate message is printed
	 * or/and error is raised.
	 * 
	 * @param changeDir String array that stores path that the user has asked for.
	 */

	@SuppressWarnings("unused")
	private void printItemLists(String[] cmd_contents) {
		
		Directory temp = currDir;
		String[] dirPath = null;
		String[] newDirPath = null;
		String itemName = null;
		int i;
		
		try {
			for (i = 1; i < cmd_contents.length; i++){
				
				dirPath = cmd_contents[i].trim().split("/");
				itemName = dirPath[dirPath.length - 1];

				try {
					
					if (dirPath.length > 1)
						if (dirPath[0].equals("")) {
							
							newDirPath = new String[dirPath.length - 2];
							System.arraycopy(dirPath, 1, newDirPath, 0, newDirPath.length);
							currDir = changeDirHelper(newDirPath, root);

						} else {

							newDirPath = new String[dirPath.length - 1];
							System.arraycopy(dirPath, 0, newDirPath, 0, newDirPath.length);
							currDir = changeDirHelper(newDirPath, currDir);
						}

					if (currDir.getContents().containsKey(itemName)) {
						
						System.out.println(itemName + ":");
						currDir.getContents().get(itemName).printContents();
					
					} else {

						System.out.print(itemName + ": No such file or ");
						System.out.println("directory exists.");
					}

					currDir = temp;
					dirPath = null;
					newDirPath = null;

				} catch (Exception e) {

					System.out.print("`"+cmd_contents[i]);
					System.out.println("\"" + ": No such directory exists.");
				}
			}

			if (cmd_contents.length == 1) {
				
				System.out.println(currDir.getName() + ":");
				currDir.printContents();
			}

		} catch (Exception e) {

			System.out.println("Invalid argument. Try 'man ls' for help.");
		}		
	}
	
	/**
	 * Prints the contents of the a file.
	 * 
	 * Prints the contents of the a file depending on user input 
	 * which is located in the working directory or the given path. 
	 * If the command is not given correctly an appropriate message is printed
	 * or/and error is raised.
	 * 
	 * @param fileNamePath String array that stores file name and/or the path.
	 */

	@SuppressWarnings("unused")
	private void printFileContents(String[] fileNamePath) {

		Directory temp = currDir;
		File store_contents = null;
		String[] dirPath = null;
		String[] newDirPath = null;
		String itemName = null;

		int i;
		try {
			
			for (i = 1; i < fileNamePath.length; i++) {

				dirPath = fileNamePath[i].trim().split("/");
				itemName = dirPath[dirPath.length - 1];

				try {
					
					if (dirPath.length > 1)
						if (dirPath[0].equals("")) {
							
							newDirPath = new String[dirPath.length - 2];
							System.arraycopy(dirPath, 1, newDirPath, 0, newDirPath.length);
							currDir = changeDirHelper(newDirPath, root);
						
						} else {
							
							newDirPath = new String[dirPath.length - 1];
							System.arraycopy(dirPath, 0, newDirPath, 0, newDirPath.length);
							currDir = changeDirHelper(newDirPath, currDir);
						}
					
					if (currDir.getContents().containsKey(itemName)){
						
						try {
							
							store_contents = (File) currDir.getContents().get(itemName);
							store_contents.printContents();

						} catch (Exception e) {

							System.out.println(itemName + ": is a directory.");
						}
					} else {

						System.out.print(itemName + ": No such file or ");
						System.out.println("directory exists.");
					}

					currDir = temp;
					dirPath = null;
					newDirPath = null;

				} catch (Exception e) {
					
					System.out.print("`"+fileNamePath[i]);
					System.out.println("\"" + ": No such directory exists.");
				}
			}
			
			fileNamePath[1] = fileNamePath[1];

		} catch (Exception e) {
			System.out.println("Invalid argument. Try 'man cat' for help.");
		}
	}
	
	/**
	 * Writes the contents to a file.
	 * 
	 * Writes the contents to a file depending on user input 
	 * which is located in the working directory or the given path.
	 * Also it takes into account whether the user wants to append 
	 * or overwrite the contents. If the command is not given correctly
	 * an appropriate message is printed or/and error is raised.
	 * 
	 * @param writeFile String array that stores the content to be written
	 *  				to the file and its path.
	 */

	@SuppressWarnings("unused")
	private void writeToFile(String[] writeFile) {
		
		Directory temp = currDir;
		File store_contents = null;
		String path = null;
		String[] dirPath = null;
		String[] newDirPath = null;
		String itemName = null;
		String contents = "";
		int i;
		int counter = 0;
		String store_type = null; 
		
		for (i =0; i<writeFile.length; i++)
			if (writeFile[i].equals(">>") || writeFile[i].equals(">"))
				counter += 1;
		
		try{
			
			writeFile[1] = writeFile[1];
			
			if (counter > 1)
				System.out.println("Too many arguments. Try 'man echo' for more help.");
			
			else if (counter == 0 && writeFile.length > 1) {
				
				for (i = 1; i < writeFile.length; i++)
					contents += writeFile[i] + " ";

				System.out.println(contents);
			
			} else {
				
				for (i = 1; i < writeFile.length; i++)
					if (writeFile[i-1].equals(">>") || writeFile[i-1].equals(">")) {
						
						path = writeFile[i].trim();
						dirPath = path.split("/");
						store_type = writeFile[i-1];
					
					} else if (!(writeFile[i].equals(">>") || writeFile[i].equals(">")))
						contents += (writeFile[i] + " ");

				try {
					
					itemName = dirPath[dirPath.length - 1];
					if (dirPath.length > 1)
						if (dirPath[0].equals("")) {

							newDirPath = new String[dirPath.length - 2];
							System.arraycopy(dirPath, 1, newDirPath, 0, newDirPath.length);
							currDir = changeDirHelper(newDirPath, root);

						} else {
							
							newDirPath = new String[dirPath.length - 1];
							System.arraycopy(dirPath, 0, newDirPath, 0, newDirPath.length);
							currDir = changeDirHelper(newDirPath, currDir);
						}

					if (currDir == null)
							currDir = temp;

					if (currDir.getContents().containsKey(itemName))
						try{
							
							store_contents = (File) currDir.getContents().get(itemName);
							
							if (store_type.equals(">>"))
								store_contents.appendContents(contents);
							else
								store_contents.setContents(contents);

						} catch (Exception e) {

							System.out.println(itemName + ": is a directory.");
						}
					else {

						store_contents = new File(itemName, currDir);
						currDir.addItem(itemName, store_contents);

						if (store_type.equals(">>"))
							store_contents.appendContents(contents);
						else
							store_contents.setContents(contents);
					}

					currDir = temp;

				} catch (Exception e) {
					
					System.out.print("`"+path);
					System.out.println("\"" + ": No such directory exists.");
				}
			}
		} catch (Exception e) {

			System.out.println("Invalid argument. Try 'man echo' for help.");
		}
	}
	
	/*
	 * Methods to be implemented in the future.
	 
	   private void move_item(String[] paths) {

	   }

       private void copy_item(String[] newpath) {

	   }
	 */
	
	private String[] process_string(String path) throws Exception {	
		int index = path.lastIndexOf("/");
		String the_path = path.substring(0,index);
		String systemItemObject = path.substring(index+1);
		String[] returnStringArray = {the_path, systemItemObject};
		return returnStringArray;
	}
	
	private void linkTo(String[] paths) throws Exception {
		Directory currTempDir = currDir;
		SystemItem objectToLink;

		if (paths[1].contains("/")) {
			String[] goToDirectory1 = {"cd", process_string(paths[1])[0]};
			changeDirectory(goToDirectory1);
		}
		
		objectToLink = currDir.getContents().get(process_string(paths[1])[1]);
		
		if (objectToLink == null) {
			System.out.println(process_string(paths[1])[1] + " does not exist");
			currDir = currTempDir;
		}
		else {
			
			currDir = currTempDir;
			
			if (paths[2].contains("/")) {
				String[] goToDirectory2 = {"cd", process_string(paths[2])[0]};
				changeDirectory(goToDirectory2);
				if (!currDir.getContents().containsKey(process_string(paths[2])[1])) {
					currDir.addItem(process_string(paths[2])[1], objectToLink);
				}
				else {
					System.out.println(process_string(paths[2])[1] + " already exists");
				}
			}
			
			else {
				if (!currDir.getContents().containsKey(paths[2])) {
					currDir.addItem(paths[2], objectToLink);
				}
				else {
					System.out.println(paths[2] + " already exists");
				}
			}
			
			currDir = currTempDir;
		}
	}
	
	private void removeItem(String[] cmdRemove) throws Exception {

		int index = 1; //starting point
		Directory currTempDir = currDir;
		Boolean check = true;
		Boolean is_path = false;
		if (cmdRemove[1].equals("-f")) {
			index = 2;
			check = false;
		}
		
		while (index < cmdRemove.length) {
			String systemItemObject;
			
			if (cmdRemove[index].contains("/")) {
				String[] goToDirectory = {"cd", process_string(cmdRemove[index])[0]};
				changeDirectory(goToDirectory);
				is_path = true; //check if this path exists
				systemItemObject = process_string(cmdRemove[index])[1];
			}
			else {
				systemItemObject = cmdRemove[index];
			}
			
			//System.out.println(systemItemObject);
			if ((currDir != currTempDir && is_path == true) || (currDir == currTempDir && is_path == false)) {
				_removeItem(systemItemObject, check);
			}
			currDir = currTempDir;
			
			index += 1;
		}
	}
	
	private void _removeItem(String name, Boolean checkForConformation) {
		if (!currDir.getContents().containsKey(name)) {
			System.out.println(name + " does not exist");
		}
		else {
			if (currDir.getContents().get(name).getClass().getName().equals("File")) {
				removeFile(name, checkForConformation);
			}
			else if (currDir.getContents().get(name).getClass().getName().equals("Directory")) {
				currDir = (Directory) currDir.getContents().get(name);
				removeDirectory(name, checkForConformation);
			}
		}
	}
	
	private void removeDirectory(String name, Boolean checkForConformation) {
		// because . and .. are in hashtable
		if (currDir.getContents().size() == 2) {
			_removeDirectory(currDir.getName(), checkForConformation);
		}
		else {
			//System.out.println(currDir.getContents().keySet());
			ArrayList<String> toDelete = new ArrayList();
			for (String content : currDir.getContents().keySet()) {
				toDelete.add(content);
			}
			
			for (String content2 : toDelete) {
				if (!content2.equals(".") && !content2.equals("..")){
					_removeItem(content2, checkForConformation);
				}
			}
			_removeDirectory(currDir.getName(), checkForConformation); //delete yourself
		}
	}
	
	private void _removeDirectory(String name, Boolean checkForConformation) {
		Scanner in = new Scanner(System.in);
		String answer;
		Directory currDirParent = currDir.getParent();
		
		if (checkForConformation == true) {
			System.out.print("Are you sure You want to delete directory " + name + " Yes(y), No(n) ");
			answer = in.nextLine();
			
			if (answer.equals("y")) {
				currDirParent.removeItem(name);
				}
			}
		
		else {
			currDirParent.removeItem(name);
		}
		currDir = currDirParent; //go back
	}
	
	private void removeFile(String name, Boolean checkForConformation) {
		Scanner in = new Scanner(System.in);
		String answer;
		
		if (checkForConformation == true) {
			System.out.print("Are you sure You want to delete file " + name + " Yes(y), No(n) ");
			answer = in.nextLine();
			
			if (answer.equals("y")) {
				currDir.removeItem(name);
			}
		}
			
		else {
			currDir.removeItem(name);
		}
	}
	/*
	private void findString(String[] dirNamePath) {
		String stringToFind = dirNamePath[1];
		Directory currTempDir = currDir;
		Boolean is_path = false;
		
		String[] goToDirectory = {"cd", dirNamePath[2]};
		changeDirectory(goToDirectory);
		is_path = true; //check if this path exists
		
		if ((currDir != currTempDir && is_path == true) || (currDir == currTempDir && is_path == false)) {
			_findString(dirNamePath[1]);
		}
		currDir = currTempDir;
	}
	
	private void _findString(String stringToFind) {
		ArrayList<String> toFind = new ArrayList(); //contents of currDir
		for (String content : currDir.getContents().keySet()) {
			toFind.add(content);
		}
		for (String content2 : toFind) {
			if (content2.getClass().getName().equals("File")) {
				findFile(stringToFind);
			}
			else if (content2.getClass().getName().equals("Directory")) {
				currDir = (Directory) currDir.getContents().get(content2);
				_findString(stringToFind);
			}
		}
	}
	
	private void findFile(String stringToFind) {
		
	}
	*/
}
